/**
 * Copyright (c) 2010-2011 Pierre Labatut
 *
 * This file is part of Space-Chronicles project.
 *
 * Space-Chronicles is free software: you can redistribute it and/or modify it
 * under the terms of its license which may be found in the accompanying
 * LICENSE file or <http://code.google.com/p/space-chronicles/>.
 */
package space.app.common;

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import space.debug.DbgError;

/**
 * This class runs a single threaded socket server, suitable to handle a single client
 * connections.
 *
 * @author Pierre
 *
 */
abstract public class Server<Message> {

    /**
     * port listening for incoming connections
     */
    private Thread serverThread;
    private final Application<Message> application;

    public Server(Application<Message> application) {
        if (application == null) {
            throw new NullPointerException();
        }
        this.application = application;
    }

    public abstract Message read(InputStream is) throws IOException;

    synchronized public boolean isRunning() {
        return serverThread != null && serverThread.isAlive();
    }

    /**
     * Start the server
     * @throws IllegalThreadStateException  if the thread was already
     *               started.
     */
    synchronized public void start(final String localIp, final int localPort) {

        if (localIp == null) {
            throw new IllegalArgumentException("Server has nt been configured");
        }
        if (localPort < 0) {
            throw new IllegalArgumentException("Server has nt been configured");
        }
        if (isRunning()) {
            throw new IllegalStateException("Server is already running");
        }


        class ServerRunnable implements Runnable {

            private Socket accept(ServerSocket ss) throws InterruptedException, IOException {

                while (true) {
                    try {
                        // 100 ms polling
                        return ss.accept();
                    } catch (InterruptedIOException iio) {
                        if (Thread.currentThread().isInterrupted()) {
                            throw new InterruptedException();
                        }
                    }
                }
            }

            public void run() {
                final InetAddress addr;
                try {
                    addr = InetAddress.getByName(localIp);
                } catch (UnknownHostException ex) {
                    throw new RuntimeException("Server stopped : Unknown address",
                            ex);
                }
                /**
                 * Open an TCP/PORT
                 */
                final ServerSocket serverSocket;
                try {
                    serverSocket = new ServerSocket(localPort, 50, addr);
                    serverSocket.setSoTimeout(100);
                } catch (IOException e) {
                    throw new RuntimeException("Unable to listen port " + localPort, e);
                }

                try {
                    try {

                        application.onServerStart(
                                "Listenning ip: " + localIp + " on port: " + localPort);
                        while (true) {


                            final Socket socket = accept(serverSocket);
                            try {
                                /**
                                 * Receiving data from incoming connection
                                 */
                                application.onServerRecieve("Connection accepted, reading data");

                                final Message msg = read(socket.getInputStream());
                                if (msg != null) {
                                    application.onServerRecieved(msg);
                                }
                            } finally {
                                socket.close();
                            }
                        }

                    } finally {
                        serverSocket.close();
                    }
                } catch (InterruptedException ex) {
                    //Gracefull interuption
                    Thread.currentThread().interrupt();
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }

                application.onServerStop("Server stopped gracefully");
            }
        }

        /**
         * Setup the server thread
         */
        serverThread = new Thread(new ServerRunnable());
        serverThread.setName("Socket Server");
        serverThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {

            public void uncaughtException(Thread t, Throwable ex) {
                DbgError.report(ex);
                application.onServerCrash(ex.getMessage());
            }
        });
        serverThread.start();
    }

    /**
     * Stop the server an wait for thread termination
     */
    synchronized public void stop() {
        if (serverThread == null) {
            throw new IllegalStateException();
        }

        application.onServerStatus("Server stopping");
        serverThread.interrupt();
        try {
            serverThread.join();
            serverThread = null;
        } catch (InterruptedException e) {
            // Restore the interruption flag
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }

        application.onServerStop("Server stopped gracefully");
    }

    void onOptionChange(Options options) {
        if (isRunning()) {
            stop();
            options.startServer(this);
        }
    }
}
